package org.hagen.mmia.yars.client;

import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.lucene.index.Term;
import org.apache.lucene.index.Terms;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.util.BytesRef;
import org.hagen.mmia.yars.agent.CollabSupport;
import org.hagen.mmia.yars.agent.Query;
import org.hagen.mmia.yars.agent.ResultList;
import org.hagen.mmia.yars.agent.utils.FloatTreeMap;
import org.hagen.mmia.yars.collection.Collection;
import org.hagen.mmia.yars.collection.Document;
import org.hagen.mmia.yars.engine.Index;

/*
 * 
 */
public class User {

	////////////////////////////////////////////////////////////////////////////////////////////////
	// Enable tracing for this module
	static final boolean	verbose = false;

	////////////////////////////////////////////////////////////////////////////////////////////////
	// Knowledge-Profile constants
	public static final int MAX_RELEVANT_TERMS_PER_DOCUMENT	= -1;

	// Skills and query constants
	public static final int MAX_TERMS_PER_USER_PROFILE = 24;
	public static final int MAX_QUERY_TERMS = 4;
	
	// Document options constants
	public static final int MAX_OVERALL_VISITS = 1;

	// Internal result set size
	public static int MAX_DOCS_PER_QUERY 	   = 1000;
	
	// User query behavior
	public static int		MAX_HITS_PER_USER  = 75;

	public static final double MIN_SCORE = 0.001;

	////////////////////////////////////////////////////////////////////////////////////////////////
	// User ID
	public static final int USER_UNASSIGNED 	= -1;
	
	public static int MAX_NO_OF_USERS		= 1;
	public static int MAX_EXPERIMENT_RUNS	= 16;

	//global user counter
	protected static double	average = 0.0;
	public int				ID 		= User.USER_UNASSIGNED;

	//term set of the user (aka skills)
	protected FloatTreeMap	skills = null;

	////////////////////////////////////////////////////////////////////////////////////////////////
	// query specific attributes
	public Query			queryIssued = null;
	public Set<String>  	     relSet = null;

	// global references
	protected CollabSupport 	doptRef = null;
	ArrayList<String>      initialQuery = null;    
	
	////////////////////////////////////////////////////////////////////////////////////////////////

	/*
	 * destructor (artificial)
	 */
	public void destruct() {
		queryIssued = null;
		ID          = User.USER_UNASSIGNED;
	}

	////////////////////////////////////////////////////////////////////////////////////////////////
	
	/*
	 * constructor
	 */
	public User( int id, CollabSupport docsOpt, Query query ) {

		this.doptRef	 = docsOpt;
		this.ID			 = id;
		this.queryIssued = null;
		this.skills      = new FloatTreeMap();
		
		this.queryIssued = query;
	}
	
	////////////////////////////////////////////////////////////////////////////////////////////////
	
	/*
	 * getter / setter
	 */
	public Query getPastQuery() {
		return queryIssued;
	}
	

	public ResultList getPastResult() {
		
		return queryIssued.getResultList();
	}

	public void setRel(Set<String> relSet) {
		this.relSet = relSet;
	}

	public void setInitialQuery(String query) {
		
		this.initialQuery = new ArrayList<String>();
		
		String terms[] = query.split( " " );

		for( String term : terms ) {
			this.initialQuery.add( term );
		}
	}
	
	////////////////////////////////////////////////////////////////////////////////////////////////

	/*
	 * knowledge profile
	 */
	public void addSkill( String term, double freq ) {
		skills.add( term, freq );
	}
	
	public void addSkills( FloatTreeMap map ) {
		
		Iterator<Entry<Object,Double>> it = map.entrySet().iterator();
		while( it.hasNext() ) {
			
			Entry<Object,Double> e = it.next();
			
			this.addSkill( (String) e.getKey(), e.getValue() );
		}
	}
	
	public void printSkills() {
		Iterator<Entry<Object, Double>> it = skills.entrySet().iterator();

		DecimalFormat format = new DecimalFormat("##0.000");
		
		System.out.print( "User::printTerms:" );
		
		int cnt = 0;
		while( it.hasNext() ) {
			Entry<Object, Double> e = it.next();
			String s = (String) e.getKey();
			Double d = (Double) e.getValue();
			System.out.print( "[" + cnt++ + "] " + s + " (" + format.format(d) + ") ");
		}
		
		System.out.print( "\n" );
	}
	
	////////////////////////////////////////////////////////////////////////////////////////////////
	
	/*
	 *  user probability for a document
	 */
	public double getProbability( String docID ) {

		Document doc = Collection.getDocument(docID);
		if( doc == null ) {
			return User.MIN_SCORE;
		}
		
		if( doc.getScore( this.ID ) > 0 ) {
			return doc.getScore( this.ID );
		}
		
		double score = User.MIN_SCORE;

		double numberOfDocuments	 = Index.numDocs;
		double documentFrequency 	 = 0.0;	
		
		long   docLength = doc.getDocumentLength();
		double avgDocLen = Index.avgDocLen;
		int    docIdx    = doc.index;

		// SIMILARITY WITH SKILLS AND QUERY
		try {
			// Term specific statistics
			Terms vector = Index.getReader().getTermVector(docIdx, Index.FIELD_CONTENT);
			// OHSUMED contains some empty documents that we skip
			if( vector == null ) {
				return score;
			}
		
			TermsEnum termsEnum = vector.iterator(null);
			
			// loop over the terms of the users (aka skill-set)
			Iterator<Entry<Object, Double>> it = skills.entrySet().iterator();
			while(it.hasNext()) {

				// get the <term,weight> tuple
				Entry<Object, Double> m = it.next();
	            String term  = (String)m.getKey();
	            Double freq = (Double)m.getValue();

	            // document frequency of the term
	            documentFrequency = Index.getReader().docFreq( new Term( Index.FIELD_CONTENT, term ) );

	            // further statistics
	            if( documentFrequency > 0 && termsEnum.seekExact( new BytesRef( term ) ) ) {

	            	// term frequency of the term
	            	int tf = (int)termsEnum.totalTermFreq();

	            	BM25 bm25 = new BM25( numberOfDocuments,
	            						  avgDocLen,
	            			  			  documentFrequency );
	            	
	            	score += bm25.weight2(tf, freq, docLength);

	            }
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		doc.setScore( this.ID, score );
		
		return score;
	}
	
	////////////////////////////////////////////////////////////////////////////////////////////////

	/*
	 * access (issue query)
	 */
	public boolean access( ArrayList<String> initialQuery ) {
		
		queryIssued.addTerms( initialQuery );

		if( verbose ) {
			queryIssued.print();
			printSkills();
		}
		
		try {
			queryIssued.execute();
		} catch (ParseException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}

		ResultList lr = queryIssued.getResultList();
		lr.setTotalHits( queryIssued.getTotalHits() );

		return true;
	}

	public boolean access( ) {
		
		return access( this.initialQuery );
	}	
	
	/*
	 * orientate (fetch some result)
	 */
	public void orientate() {

		ArrayList<String> doclist = doptRef.fetch( this );
		
		orientate( doclist );
	}

	/*
	 * 
	 */
	private void orientate( ArrayList<String> doclist ) {

		ResultList lr = getPastResult();
		lr.setTotalHits( queryIssued.getTotalHits() );
		
		for( String docID : doclist ) {
			
			lr.addViewed( docID );
			if( verbose ) {
				System.out.print( "user" + ID + " " +  "viewed document " + docID + " ");
			}

			if( this.relSet.contains( docID ) ) {
				lr.addRelevant( docID );
				if( verbose ) {
					System.out.print( " x " );
				}
			} else {
				lr.addNotRelevant( docID );
			}
			
			if( verbose ) {
				System.out.println( " " );
			}			
		}
	}
}

/*
 * 
 */
class BM25 {
	/** The constant k_1.*/
	private double k_1 = 1.2d;
    private double k_3 = 100;
	
	/** The parameter b.*/
	private double b=0.75d;
	
	private double numberOfDocuments = 0.0;
	private double averageDocumentLength = 0.0;
	private double documentFrequency = 0.0;	
			
	BM25( double numberOfDocuments,
		  double averageDocumentLength,
		  double documentFrequency ) {
	
		this.numberOfDocuments 		= numberOfDocuments;
		this.averageDocumentLength	= averageDocumentLength;
		this.documentFrequency	    = documentFrequency;
	}

	/*
	 * 
	 */
	public final double weight(double tf, double docLength) {
	    double K = k_1 * ((1 - b) + b * docLength / averageDocumentLength) + tf;

	    return (tf / K)* 
	    		Math.log10( (numberOfDocuments - documentFrequency + 0.5d) /
	    					(documentFrequency + 0.5d) );
	}
	
	public final double weight2(double tf_doc, double tf_profile, double docLength) {
		
		double K1 = ( k_3 + 1 ) * tf_profile / ( k_3 + tf_profile );
		
		double K2 = ( k_1 + 1 ) * tf_doc /
					( k_1 * ((1 - b) + b * docLength / averageDocumentLength) + tf_doc );

	    return K1 * K2 *
	    		Math.log10( (numberOfDocuments - documentFrequency + 0.5d) /
    					    (documentFrequency + 0.5d) );
	}
}
